import {Terminals} from '../lexer/terminal.js';
import {Tool} from '../common/tool.js';
import {production} from './production.js';

/**
 * represents a formal grammar, encapsulating the set of production rules 
 * that define the syntax of a language. This function provides the mechanisms 
 * to derive strings within the language and to analyze the syntactic structure 
 * of those strings based on the defined production rules.
 * eg: statement -> let_statement | var_statement |...
 * statement is a grammar,
 * statement -> let_statement is a production
 * statement -> var_statement is a production
 * huizi 2024
 */
function grammar(grammars, nonTerminal){
    const productions = [];
    /**
     * the "follow" set of a non-terminal in a grammar is the set of terminals that can appear immediately to the right of the non-terminal
     * place the end-of-input marker ($) in the follow set of the start symbol.
     * if there is a production rule X → ...Aa..., where A is a non-terminal and a is a terminal, then add a to the follow set of A.
     * if there is a production rule X → ...AB... or X → ...A, where A and B are non-terminals, then everything in the first set of B except ε is in the follow set of A. If B can derive ε or if A is at the end, then everything in the follow set of X is also in the follow set of A.
     */
    const follow = [];
    if(nonTerminal.isStart()){
        follow.push(Terminals.END);
    }

    /**
     * the "first" set of a non-terminal in a grammar is the set of terminals that can appear at the beginning
     * if there is a production rule X → a..., where a is a terminal, then a is in the first set of X.
     * if there is a production rule X → ε (where ε represents the empty string), then ε is in the first set of X.
     * if there is a production rule X → Y..., where Y is a non-terminal, then add the first set of Y to the first set of X (excluding ε unless Y can derive ε).
    */
    function first(){
        const firsts = [];
        this.productions.forEach(production => {
            const duplicated = firsts.filter(f=>production.first().includes(f)).filter(f=>f===Terminals.EPSILON);
            if(duplicated.length>0){
                Tool.grammarErr(`there is duplicate production first: ${duplicated} in grammar: ${nonTerminal.name()}, which will cause FIRST-FIRST conflict in LL(1) parser`);
            }
            firsts = firsts.concat(production.first());
        })
        return firsts;
    }

    return Object.freeze({
        /**
         * the non terminal type of the grammar
         */
        type: ()=> nonTerminal,
        /**
         * first of the grammar
         */
        first: first,
        /**
         * follow of the grammar
         * will be filled by other related grammars
         */
        follow: ()=> follow.filter(f=>f===Terminals.EPSILON),
        /**
         * add production for the grammar
         */
        addProduction: function (productionStr){ productions.push(production(this,productionStr));},
        /**
         * return the grammar manager where all grammars are stored
         */
        grammars: ()=> grammars,
        /**
         * get all productions under the grammar
         */
        productions: ()=> productions
    });
};

export {grammar};